// Copyright (c) 2024, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:js_interop';
import 'dart:typed_data';
import 'dart:convert';

import 'package:source_maps/parser.dart';

final isMinified = const bool.fromEnvironment('dart.tool.dart2wasm.minify');

void f() {
  g();
}

void g() {
  throw 'hi';
}

runtimeFalse() => int.parse('1') == 0;

// `expectedFrames` is (file, line, column, name) of the frames we check.
//
// Information we don't check are "null": we don't want to check line/column
// of standard library functions to avoid breaking the test with unrelated
// changes to the standard library.
void testMain(
  String testName,
  List<(String?, int?, int?, String?)?> expectedFrames,
) {
  // Use `f` and `g` in a few places to make sure wasm-opt won't inline them
  // in the test.
  final fTearOff = f;
  final gTearOff = g;

  if (runtimeFalse()) f();
  if (runtimeFalse()) g();

  // Get some simple stack trace.
  String? stackTraceString;
  try {
    f();
  } catch (e, st) {
    stackTraceString = st.toString();
  }

  // Print the stack trace to make it easy to update the test.
  print("-----");
  print(stackTraceString);
  print("-----");

  final actualFrames = parseStack(
    testName,
    getSourceMapping(testName),
    stackTraceString!,
  );
  print('Got stack trace:');
  for (final frame in actualFrames) {
    print('  $frame');
  }
  print('Matching against:');
  for (final frame in expectedFrames) {
    print('  $frame');
  }

  if (actualFrames.length < expectedFrames.length) {
    throw 'Less actual frames than expected';
  }

  for (int i = 0; i < expectedFrames.length; i++) {
    final expected = expectedFrames[i];
    final actual = actualFrames[i];
    if (expected == null) continue;
    if (actual == null) {
      throw 'Mismatch:\n  Expected: $expected\n  Actual: <no mapping>';
    }
    if ((expected.$1 != null && actual.$1 != expected.$1) ||
        (expected.$2 != null && actual.$2 != expected.$2) ||
        (expected.$3 != null && actual.$3 != expected.$3) ||
        (expected.$4 != null && actual.$4 != expected.$4)) {
      throw 'Mismatch:\n  Expected: $expected\n  Actual: $actual';
    }
  }
}

String getFilename(String testName, int moduleId) {
  final compilationDir = const String.fromEnvironment("TEST_COMPILATION_DIR");
  if (moduleId == 0) {
    return '$compilationDir/${testName}_test.wasm.map';
  } else {
    return '$compilationDir/${testName}_test_module$moduleId.wasm.map';
  }
}

Mapping getSourceMapping(String testName) {
  final allMappings = MappingBundle();
  // Read source map of the current program.
  final compilationDir = const String.fromEnvironment("TEST_COMPILATION_DIR");
  final mainFilename = getFilename(testName, 0);
  final mainSourceMapFile = readfile(mainFilename);
  final mainSourceMap = parse(utf8.decode(mainSourceMapFile)) as SingleMapping;
  mainSourceMap.targetUrl = mainFilename;
  allMappings.addMapping(mainSourceMap);

  int i = 1;
  while (true) {
    // All the modules will have consecutive names. Keep reading them until we
    // fail to find one.
    try {
      final filename = getFilename(testName, i);
      final fileContents = readfile(filename);
      final sourceMap = parse(utf8.decode(fileContents)) as SingleMapping;
      sourceMap.targetUrl = filename;
      allMappings.addMapping(sourceMap);
      i++;
    } catch (e) {
      break;
    }
  }
  return allMappings;
}

List<(String?, int?, int?, String?)?> parseStack(
  String testName,
  Mapping mapping,
  String stackTraceString,
) {
  final parsed = <(String?, int?, int?, String?)?>[];
  for (final line in stackTraceString.split('\n')) {
    if (line.contains('.mjs') || line.contains('.js')) {
      parsed.add(null);
      continue;
    }

    final hexOffsetMatch = stackTraceHexOffsetRegExp.firstMatch(line);
    if (hexOffsetMatch == null) {
      throw 'Unable to parse hex offset in frame "$line"';
    }
    final hexOffsetStr = hexOffsetMatch.group(1)!; // includes '0x'
    final offset = int.tryParse(hexOffsetStr);
    if (offset == null) {
      throw 'Unable to parse hex number in frame "$line"';
    }
    final moduleIdMatch = stackTraceModuleNameRegExp.firstMatch(line);
    if (moduleIdMatch == null) {
      throw 'Unable to parse module name in frame "$line"';
    }
    final moduleIdString = moduleIdMatch.group(1)!;
    final moduleId = isMinified
        ? parseMinifiedModule(moduleIdString)
        : int.parse(moduleIdString.replaceAll('module', ''));
    final uri = getFilename(testName, moduleId);
    final span = mapping.spanFor(0, offset, uri: uri);
    if (span == null) {
      print('Stack frame "$line" does not have a source mapping');
      parsed.add(null);
      continue;
    }
    final filename = span.sourceUrl!.pathSegments.last;
    final lineNumber = span.start.line;
    final columnNumber = span.start.column;
    final symbolName = span.text;
    parsed.add((filename, 1 + lineNumber, 1 + columnNumber, symbolName));
  }
  return parsed;
}

int parseMinifiedModule(String moduleName) {
  final codeUnits = moduleName.codeUnits;
  int result = 0;
  int power = 1;
  for (final codeUnit in codeUnits) {
    result += codeUnit * power;
    power *= 128;
  }
  return result - 1;
}

/// Read the file at the given [path].
///
/// This relies on the `readbuffer` function provided by d8.
@JS()
external JSArrayBuffer readbuffer(JSString path);

/// Read the file at the given [path].
Uint8List readfile(String path) => Uint8List.view(readbuffer(path.toJS).toDart);

final stackTraceHexOffsetRegExp = RegExp(r'wasm-function.*(0x[0-9a-fA-F]+)\)$');
final stackTraceModuleNameRegExp = RegExp(r'wasm/(.*)-[0-9a-z]+:');
